HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { notFound } from 'next/navigation';4import { AdminTitle, Bool, JsonPre, Mono, StatusChip } from '@/components/admin/ui';5import { TierBadge } from '@/components/ui/badges';6import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table';7import { KeyValue } from '@/components/ui/key-value';8import { LiveAgo } from '@/components/ui/live';9import { Unavailable } from '@/components/ui/unavailable';10import { adminApi, load, requireAdmin } from '@/lib/admin/admin-api';11import type { AdminSnapshotRow } from '@/lib/admin/types';12import { fmtBytes, fmtDateTime, fmtInt, num } from '@/lib/format';13import { routes } from '@/lib/site';1415export const metadata: Metadata = { title: 'Document', robots: { index: false, follow: false } };16export const dynamic = 'force-dynamic';1718export default async function AdminDocumentPage({ params }: { params: Promise<{ id: string }> }) {19 await requireAdmin();20 const { id } = await params;21 const res = await load(adminApi.document(id));22 if (!res.ok && res.error.startsWith('404')) notFound();23 if (!res.ok) {24 return (25 <>26 <AdminTitle title="Document" />27 <Unavailable what="Document" reason={res.error} />28 </>29 );30 }31 const d = res.data;32 const snaps: AdminSnapshotRow[] = Array.isArray(d.snapshots) ? d.snapshots : [];33 const meta = { ...(d.meta ?? {}) } as Record<string, unknown>;34 delete meta.raw_path;35 delete meta.text_path;36 return (37 <>38 <p className="mb-2 text-xs"><Link href="/admin/documents" className="link">← Documents</Link></p>39 <AdminTitle title={d.title ?? d.url.replace(/^https?:\/\/(www\.)?/, '')} lede={<Mono>{d.id}</Mono>} />40 <div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_20rem]">41 <div className="min-w-0">42 <KeyValue43 dense44 rows={[45 { key: 'url', label: 'URL', value: <a href={d.url} target="_blank" rel="noopener noreferrer" className="link break-all">{d.url}</a> },46 { key: 'canonical_url', label: 'Canonical', raw: d.canonical_url },47 { key: 'connector', label: 'Connector', value: <Link href={`/admin/documents?connector=${encodeURIComponent(d.connector_name ?? '')}`} className="mono text-xs hover:text-accent">{d.connector_name ?? '—'}</Link> },48 { key: 'source', label: 'Source', value: <span className="text-sm">{d.source_name ?? d.source_id ?? '—'} {d.source_tier !== undefined && d.source_tier !== null && <TierBadge tier={num(d.source_tier)} />}</span> },49 { key: 'doc_type', label: 'Type', raw: d.doc_type },50 { key: 'entity', label: 'Entity', value: d.entity_slug && d.entity_type ? <Link href={routes.entity({ entity_type: d.entity_type, slug: d.entity_slug })} className="link">{d.entity_name ?? d.entity_slug}</Link> : <span className="text-ink-3">—</span> },51 { key: 'status', label: 'Status', value: <StatusChip value={d.status} /> },52 { key: 'processing', label: 'Processing', value: <StatusChip value={d.last_processing_status ?? null} /> },53 { key: 'first_seen_at', label: 'First seen', raw: d.first_seen_at ? fmtDateTime(d.first_seen_at) : null },54 { key: 'last_fetched_at', label: 'Last fetched', raw: d.last_fetched_at ? fmtDateTime(d.last_fetched_at) : null },55 { key: 'last_changed_at', label: 'Last changed', raw: d.last_changed_at ? fmtDateTime(d.last_changed_at) : null },56 { key: 'next_fetch_at', label: 'Next fetch', raw: d.next_fetch_at ? fmtDateTime(d.next_fetch_at) : null },57 { key: 'counts', label: 'Fetch / change / fail', value: <span className="tnum">{fmtInt(d.fetch_count)} / {fmtInt(d.change_count)} / {fmtInt(d.fail_count)}</span> },58 { key: 'last_status', label: 'Last HTTP', raw: d.last_status },59 { key: 'etag', label: 'ETag', value: d.etag ? <Mono>{d.etag}</Mono> : undefined },60 { key: 'content_hash', label: 'Content hash', value: d.content_hash ? <Mono className="break-all">{d.content_hash}</Mono> : undefined },61 { key: 'priority', label: 'Priority', raw: d.priority },62 { key: 'needs_llm', label: 'Needs LLM', value: <Bool v={d.needs_llm} /> },63 ]}64 />65 </div>66 <aside className="min-w-0">67 <p className="eyebrow mb-2">Meta</p>68 <JsonPre value={Object.keys(meta).length ? meta : null} maxHeight="20rem" />69 </aside>70 </div>7172 <section className="mt-8">73 <p className="eyebrow mb-2">Snapshots <span className="tnum text-ink-3">{snaps.length}</span></p>74 <DataTable compact scroll caption="Snapshots">75 <thead>76 <tr>77 <Th>Observed</Th>78 <Th num>HTTP</Th>79 <Th num>Bytes</Th>80 <Th>Changed</Th>81 <Th>Processing</Th>82 <Th>Parser</Th>83 <Th>Transport</Th>84 <Th>Structured</Th>85 <Th>Text</Th>86 <Th>Run</Th>87 </tr>88 </thead>89 <tbody>90 {snaps.length === 0 && <EmptyRow cols={10}>No snapshots archived for this document.</EmptyRow>}91 {snaps.map((s) => (92 <tr key={s.id}>93 <Td>94 <Link href={`/admin/snapshots/${encodeURIComponent(s.id)}`} className="text-xs text-ink hover:text-accent" title={s.id}>95 {fmtDateTime(s.observed_at)} · <LiveAgo at={s.observed_at} />96 </Link>97 </Td>98 <Td num className={`tnum text-xs ${(num(s.http_status) ?? 0) >= 400 ? 'text-danger' : ''}`}>{fmtInt(s.http_status)}</Td>99 <Td num className="tnum text-xs">{fmtBytes(s.byte_size)}</Td>100 <Td><Bool v={s.changed} /></Td>101 <Td><StatusChip value={s.processing_status} /></Td>102 <Td><Mono>v{s.parser_version ?? '?'}</Mono></Td>103 <Td><Mono>{s.transport ?? '—'}</Mono></Td>104 <Td><Bool v={s.has_structured} /></Td>105 <Td><Bool v={s.has_text} /></Td>106 <Td>{s.run_id ? <Mono>{s.run_id}</Mono> : <span className="text-ink-3">—</span>}</Td>107 </tr>108 ))}109 </tbody>110 </DataTable>111 </section>112 </>113 );114}115